;*******************************************************
; EXT2.asm
; loads the appropriate Railgun kernel
;*******************************************************

[ORG 0x7c00]
[BITS 16]

; long jump to align segments
jmp	0x0000:START

;*******************************************************
; data

partition_offset	dd 0
drive_number		db 0

; filesystem stuff
sectors_per_block	dd 0
inodes_per_block	dd 0
block_size		dd 0
inode_size		dd 128		; default is 128 for version < 1.0
inodes_per_group	dd 0

boot_dir		db "Railgun"
boot_file64		db "Railgun64.sys"
boot_file32		db "Railgun32.sys"
file64_inode		dd 0
file32_inode		dd 0
long_mode		dw 0		; set to 1 if long mode is detected, then is set to 0 if no 64 bit kernel

msgNoRailgun		db "1"		; error code 1: could not find boot directory "Railgun" in boot partition
msgNoKernel		db "2"		; error code 2: neither 64 bit or 32 bit kernels are present or 32 bit kernel is not present when only 32 bit mode is possible
msgNoA20		db "3"		; error code 3: could not open A-20 gate using fast A20 method
msgNoMemoryMap		db "4"		; error code 4: could not generate memory map. Try updating BIOS?
msgDriveError		db "5"		; error code 5: could not read from drive. general read error.

;***************************************************************
; protected mode flat GDT

prmd_gdt_table:
	; null
	dq 0
	
	; kernel code
	dw 0xFFFF
	dw 0x0000
	db 0x00
	db 10011010b
	db 11001111b
	db 0x00
	
	; kernel data
	dw 0xFFFF
	dw 0x0000
	db 0x00
	db 10010010b
	db 11001111b
	db 0x00

prmd_gdt_desc:
	dw $ - prmd_gdt_table - 1
	dd prmd_gdt_table

;****************************************************************
; long mode flat GDT

long_gdt_table:
	; null
	dq 0
	
	; kernel code
	dw 0
	dw 0
	db 0
	db 10011000b
	db 00100000b
	db 0
	
	; kernel data
	dw 0
	dw 0
	db 0
	db 10010000b
	db 00000000b
	db 0
	
long_gdt_desc:
	dw $ - long_gdt_table - 1
	dq long_gdt_table
	
;*****************************************************************
; functions

;*******************************
; compare 2 null-terminated strings
; si = ptr to string 1
; di = ptr to string 2
; cx = length of string
; sets carry on fail

CompString:
	; si and di point to the chars
	; save di
	push	di

.loop:
	; check the char
	mov	al, [si]
	mov	bl, [di]
	inc	si
	inc	di
	cmp	al, bl
	jne	.fail
	loop	.loop

.success:
	pop	di
	clc
	ret

.fail:
	pop	di
	stc
	ret

	
	
;*******************************
; load a block
; eax = block to load
; bx:si = target start address

LoadBlock:
	; save gprs
	pusha
	
	; calculate the start sector
	; start sector = block number * sectors per block
	mov	ecx, [sectors_per_block]
	mul	ecx
	
	; calculate the sector count
	; sector count = sectors per block
	mov	cx, [sectors_per_block]
	
	; call the function
	call	LoadSectors
	
	; return
	popa
	ret

;*******************************
; new drive read function
; eax = start lba
; cx = count
; bx:si = target start address

LoadSectors:
	; fetch the drive number
	mov	dl, [drive_number]

	; add sector offset for partition start
	add	eax, [partition_offset]

	; put DAP values onto the stack
	; must be done in REVERSE order
	push	DWORD 0		; DAP sector start high 32 bits
	push	eax		; DAP sector start low 32 bits
	push	bx		; DAP segment
	push	si		; DAP offset
	push	cx		; DAP sector count
	push	WORD 0x0010	; DAP size + unused
	
	; save the pointer of the top of the stack
	push	sp
	
	; setup for the loop
	mov	di, 10		; 10 attempts

.loop:
	; decrease loop counter
	dec	di

	; check if counter = 0
	or	di, di
	jz	.fail

	; attempt to reset the drive
	; setup for drive reset
	mov	ah, 0

	; call the interrupt
	int	0x13

	; if reset failed, try again
	jc	.loop

	; drive reset was success, now try the read
	; setup for extended read
	pop	si		; get the pointer to the DAP
	push	ds		; save ds
	mov	ax, ss		; get the segment of the DAP
	mov	ds, ax
	mov	ah, 0x42	; extended read

	; call the interrrupt
	int	0x13

	; restore ds
	pop	ds
	
	; if read failed, try again
	jc	.loop

	; restore the stack
	add	sp, 16

.finish:
	; finished - return
	ret
	
.fail:
	; failed - print error message and halt
	mov	si, msgDriveError
	jmp	PrintError

;*******************************
; error print function. only prints first 2 chars. and halts
; si = pointer to string to print

PrintError:
	; setup registers for video out mode
	mov	ax, 0x0e00 + 'E'
	mov	bx, 0x0007
	int	0x10		; call the print function
	lodsb			; load second char
	int	0x10		; call the print function
	
	; stop
	cli
	hlt

;**********************************************************************************

START:
	; setup segment registers and stack
	mov	bx, cs
	mov	ds, bx
	mov	es, bx
	mov	ss, bx
	mov	sp, 0x1000

GETPARTITION:
	; ds:si might point to partition entry, check it
	mov	al, [si]
	and	al, 0x80
	jz	.continue
	
	; get partition offset
	mov	eax, [si + 8]
	mov	[partition_offset], eax
	
.continue:
	; save the drive number
	mov	[drive_number], dl

LOADSECONDSECTOR:
	; need to load the second sector to 0x7e00
	mov	eax, 1		; want second sector
	mov	cx, 1		; just 1
	mov	si, 0x7e00	; to here
	call	LoadSectors

OPENA20GATE:
	; use the fast A20 open option
	mov	si, msgNoA20	; get the error message just in case
	mov	ax, 0x2401	; ax = 0x2401 for fast A20
	int	0x15		; call it
	jc	PrintError	; error out on error
	
GETFSINFO:
	; load superblock (sectors 2 and 3) to 0x8000
	mov	ax, 2		; start at sector 2
	mov	cx, 2		; load 2 sectors
	mov	bx, 0		; segment 0
	mov	si, 0x8000	; set target address
	call	LoadSectors
	
	; get inodes per group
	mov	ax, [0x8000 + 40]
	mov	[inodes_per_group], ax
	
	; get inode size (version number dependant)
	mov	ax, [0x8000 + 76]		; get major version number
	or	ax, ax				; check it for 0
	jz	.continue			; if zero skip next bit	
	mov	ax, [0x8000 + 88]		; get the inode size from extended superblock
	mov	[inode_size], ax		; save it
	
.continue:
	; get block size and sectors per block
	mov	cx, [0x8000 + 24]		; get number to shift 1024 left by for block size
	mov	ax, 1024			; put 1024 in eax
	shl	ax, cl				; shift it left by fetched number
	mov	[block_size], ax		; save block size
	shr	ax, 9				; divide by 512 to get sectors per block
	mov	[sectors_per_block], ax		; save sectors per block
	
	; calculate inodes per block
	shl	ax, 9				; get block size
	xor	dx, dx
	div	WORD [inode_size]		; divide by inode size
	mov	[inodes_per_block], ax		; save it
	
LOADBLOCKGROUPDESCRIPTORTABLE:
	; load the block group descriptor table to 0x8400
	; block group descriptor table is block 0 for block size = 1024 or block 1 for block size > 1024
	mov	eax, [sectors_per_block]
	mov	bx, 1		; default is block 1
	cmp	al, 2		; check ax for 2 sectors per block (block size = 1024)
	jne	.continue	; if not 2 sectors per block then skip next bit
	inc	bx		; increase bx to block 2
	
.continue:
	; read the block to 0x8400
	mov	si, 0x8400
	mov	ax, bx
	xor	bx, bx
	call	LoadBlock
	
GETROOTINODEPOINTER:
	; first get starting block for inode table containing the root inode (2)
	mov	eax, 2
	call	LoadInode
	
GETROOTINODEBLOCK:
	; load the first block containing the root inode directories to 0x9000
	; NOTE: only the first block is parsed for the boot directory, so if it is not found
	; reformat the partition and try again
	; si = ptr to inode structure
	mov	eax, [si + 40]	; direct block pointer 0
	mov	si, 0x9000
	call	LoadBlock
	
PARSEROOTINODEBLOCK:
	; parse the block to find the boot directory
	mov	si, 0x9000
	mov	di, boot_dir
	call	FindFile
	jnc	.found_railgun
	
	; could not find the railgun directory
	mov	si, msgNoRailgun
	jmp	PrintError
	
.found_railgun:
	; found the railgun directory
	; eax = inode number
	; load the inode
	call	LoadInode
	
	; si = address of inode
	; get inode block
	mov	eax, [si + 40]
	
	; load this block to 0x9000
	mov	si, 0x9000
	xor	bx, bx
	call	LoadBlock
	
	PARSEFORLOADERS:
	; parse the block for the 2 loader files in turn
	mov	si, 0x9000			; si points to start of block
	mov	di, boot_file32			; pointer to 32 bit kernel file name
	call	FindFile
	jc	.no_32
	
.found_32:
	; found the railgun 32 kernel file
	; eax = inode
	mov	[file32_inode], eax
	
.no_32:
	; now look for the railgun 64 kernel
	mov	si, 0x9000		; reset si to start of block
	mov	di, boot_file64		; pointer to 64 bit kernel file name
	call	FindFile
	jc	.no_64
	
.found_64:
	; found the railgun 64 kernel file
	; eax = inode number
	mov	[file64_inode], eax
	
.no_64:	

CHECKLONGMODE:
	; use CPUID to check if long mode is supported
	; check for cpuid extentions
	mov 	eax, 0x80000000 	; Set the A-register to 0x80000000.
	cpuid                  		; CPU identification.
    	cmp 	eax, 0x80000001		; Compare the A-register with 0x80000001.
    	jb 	NO_LONG_MODE		; It is less, there is no long mode.
    	
	; check for long mode
	mov	eax, 0x80000001		; Set the A-register to 0x80000001.
	cpuid                  		; CPU identification.
	test	edx, 1 << 29      	; Test if the LM-bit, which is bit 29, is set in the D-register.
	jz	NO_LONG_MODE         	; They aren't, there is no long mode.

	; long mode is supported
	mov	[long_mode], BYTE 1

NO_LONG_MODE:
    	
	; jump over gap
	jmp	OVERGAP

;*********************************************************
; bootsig
times 510 - ($ - $$) db 0
dw 0xaa55
;*********************************************************

OVERGAP:
	; now need to decide which kernel to load
	; if no long mode, load 32 bit kernel
	; if no 64 bit kernel, load 32 bit kernel
	; else, load 64 bit kernel
	mov	eax, [file32_inode]	; load the 32 bit kernel inode into eax
	mov	bx, [long_mode]		; load the long mode indicator into bx
	or	bl, bl			; check it for zero
	jz	LOADKERNEL		; if long mode indicator is zero, use the 32 bit kernel inode
	
	; check for 64 bit kernel
	mov	[long_mode], WORD 0	; set long mode indicator to 0
	mov	ebx, [file64_inode]	; load the 64 bit inode
	or	ebx, ebx		; check it for zero
	jz	LOADKERNEL		; if zero, use the 32 bit kernel
	
	; use 64 bit kernel
	mov	eax, ebx		; otherwise use the 64 bit kernel
	mov	[long_mode], WORD 1	; set long mode indicator to 1
	
LOADKERNEL:
	; check if the inode given in eax is nonzero	
	or	eax, eax
	jnz	.continue
	
	; print error
	mov	si, msgNoKernel
	jmp	PrintError
	
.continue:
	; inode to load is in eax.
	call	LoadInode
	
LOADFILE:
	; si = address of inode
	; load the file to 0x1000:0x0000
	; start by loading 12 direct block pointers
	; set di to the first direct block pointer and set the target address
	mov	di, si			; get base address of inode in di
	add	di, 40			; add 40 to point to the first direct block pointer
	mov	si, 0x0000		; clear si
	mov	bx, 0x1000		; set target segment
	mov	cx, 12			; loop max 12 times
	
.direct_loop:
	; bx:si points to destination
	; di points to direct block to load
	mov	eax, [di]			; get block number
	or	eax, eax			; check it for zero
	jz	.done				; if zero finish
	call	LoadBlock			; otherwise load it
	add	si, WORD [block_size]		; add the block size to si (will wrap if file is larger than 64k)
	add	di, 4				; point di to next entry
	loop	.direct_loop			; get next one
	
.load_singly_indirect:
	; run out of direct blocks
	; load indirect block pointed by di
	mov	eax, [di]		; get pointer to indirect block
	or	eax, eax		; check for zero
	jz	.done			; if zero done
	push	si			; save si
	push	bx			; save bx
	mov	bx, 0x0000		; save singly indirect block to 0x9000
	mov	si, 0x9000
	call	LoadBlock
	
	; setup for singly indirect load loop
	mov	di, 0x9000		; point di to the first entry
	pop	bx			; restore target segment
	pop	si			; restore target offset
	
.singly_indirect_loop:
	; bx:si points to destination
	; di points to direct block to load
	mov	eax, [di]		; load the block number
	or	eax, eax		; check it for zero
	jz	.done			; if zero finished
	call	LoadBlock		; else load the block
	add	si, WORD [block_size]	; add the block size to the destination counter
	add	di, 4			; make di point to next block
	jmp	.singly_indirect_loop	; get the next one

.done:
	; finished loading

GETMEMORYMAP:
	; create a memory map at 0x0000:0xe000
	; and memory map entry count at 0x0000:0xfff0
	;mov	di, 0x0000
	;mov	es, di
	mov	di, 0xe000
	call	do_e820
	
	; save entry count
	mov	[0xfff0], bp
	
SETUPPMODE:
	; setup for mininal protected mode environment
	cli				; disable interrupts
	lgdt	[prmd_gdt_desc]		; load basic GDT
	mov	eax, cr0		; get cr0
	or	al, 1			; set protected mode switch
	mov	cr0, eax		; save it back in cr0
	jmp	0x08:START32		; jump to 32 bit segment to enable 32 bit protected mode

;*************************************************************************************************
[BITS 32]
START32:
	; setup the data segment
	mov	ax, 0x10
	mov	ds, ax

	; check if using long mode
	mov	ax, [long_mode]		; get long mode indicator
	or	al, al			; check for 0
	jnz	.enable_long_mode	; if zero, go to long mode enabling section
	jmp	0x10000			; else jump to the 32 bit protected mode kernel
	
.enable_long_mode:
	; loaded a 64 bit kernel, so using long mode
	; need to create basic page tables (identity map the first 2 mb)
	; put the page table at 0x50000
	mov	edi, 0x50000		; put the plm4 address in edi
	mov	cr3, edi		; give cr3 the location of the page table pml4
	
	; create a pml4 entry
	mov	eax, 0x1b + 0x51000	; flags + address of pdpe entry
	xor	ebx, ebx		; set high half to 0
	mov	[edi], eax		; write low half
	mov	[edi + 4], ebx		; write high half
	
	; create a pdpe entry at 0x51000
	mov	di, 0x1000
	mov	ax, 0x201b		; flags + low part of address (high part is the same)
	mov	[edi], eax		; write low half
	mov	[edi + 4], ebx		; write high half
	
	; create a pde entry at 0x52000
	mov	di, 0x2000
	mov	eax, 0x9b		; flags + 0 = identity map 1st 2mb
	mov	[edi], eax		; write low half
	mov	[edi + 4], ebx		; write high half
	
	; enable PAE
	mov	eax, cr4
	or	al, 1 << 5
	mov	cr4, eax
	
	; enable long mode
	mov	ecx, 0xc0000080
	rdmsr
	or	ax, 1 << 8
	wrmsr
	
	; enable paging
	mov	eax, cr0
	or	eax, 1 << 31
	mov	cr0, eax
	
	; load the long mode GDT
	lgdt	[long_gdt_desc]
	
	; jump to long mode code
	jmp	0x08:0x10000
	
;*************************************************************************************************
[BITS 16]
;*************************************************************************************************
; function to find a file in a directory block
; si = ptr to start of directory entries
; di = ptr to filename to find
; returns eax = inode on success, garbage on error
; returns carry set on error, clear on success

FindFile:
	; si points to directory entries start
	; di points to file name string
	
.loop:
	; the main parse loop
	; si points to the start of the directory entry
	; check it equals zero
	mov	eax, [si]
	or	eax, eax
	jz	.fail
	
	; directory entry exists
	; save entry pointer
	push	si
	
	; check the directory name
	movzx	cx, BYTE [si + 6]	; get the length of the entry
	add	si, 8			; get pointer to directory string
	call	CompString		; run comparison
	jnc	.success		; leave if directory is found
	pop	si			; restore entry pointer
	
	; move si to next directory
	mov	ax, [si + 4]		; get total size of this entry
	add	si, ax			; add it to the pointer
	jmp	.loop			; move to next entry
	
.fail:
	; set carry and return
	stc
	ret
	
.success:
	; get inode
	pop	si
	mov	eax, [si]

	; clear carry and return
	clc
	ret



;*************************************************************************************************
; function to load an inode data block
; eax = inode to load
; returns si = ptr to inode structure

LoadInode:
	; calculate which block group the inode is in and its index within that group
	; block group = (inode - 1) / INODES_PER_GROUP
	; index = (inode - 1) % INODES_PER_GROUP
	dec	eax				; inode - 1
	xor	edx, edx			; clear edx
	div	DWORD [inodes_per_group]	; / INODES_PER_GROUP
	mov	edi, edx			; save index to edi
		
	; find the starting block of inode table for calculated block group
	; block group in eax
	shl	ax, 5				; block group * 32 = offset into block group descriptor
	mov	si, 0x8400			; get pointer to block group descriptor table
	add	si, ax				; add offset to it
	mov	ebx, [si + 8]			; get starting block number into ebx
	
	; calculate the containing block number for the index
	; containing block = (index * INODE_SIZE) / BLOCK_SIZE
	mov	eax, edi			; get index
	mul	DWORD [inode_size]		; * INODE_SIZE
	div	DWORD [block_size]		; / BLOCK_SIZE
	
	; add it to the starting block
	add	eax, ebx			; add starting block to the containing block
	
	; load the block to 0x8800
	push	edi
	mov	si, 0x8800
	xor	bx, bx
	call	LoadBlock
	pop	edi
	
	; get index into block
	; index into block = (inode_index % INODES_PER_BLOCK) * INODE_SIZE 
	xor	edx, edx			; clear edx
	mov	eax, edi			; get index
	div	DWORD [inodes_per_block]	; % INODES_PER_BLOCK
	mov	eax, edx
	mul	DWORD [inode_size]		; * INODE_SIZE
	mov	si, 0x8800			; get base address
	add	si, ax				; add index
	
	; return
	ret


;*************************************************************************************************
; function to get the memory map via BIOS int 0x15:e820
; used from OSDev wiki with thanks :) http://wiki.osdev.org/Detecting_Memory_%28x86%29#Getting_an_E820_Memory_Map
; use the INT 0x15, eax= 0xE820 BIOS function to get a memory map
; inputs: es:di -> destination buffer for 24 byte entries
; outputs: bp = entry count, trashes all registers except esi

do_e820:
	xor 	ebx, ebx		; ebx must be 0 to start
	xor	bp, bp			; keep an entry count in bp
	mov 	edx, 0x0534D4150	; Place "SMAP" into edx
	mov 	eax, 0xe820
	mov 	[di + 20], dword 1	; force a valid ACPI 3.X entry
	mov 	ecx, 24			; ask for 24 bytes
	int 	0x15
	jc 	short .failed		; carry set on first call means "unsupported function"
	mov 	edx, 0x0534D4150	; Some BIOSes apparently trash this register?
	cmp 	eax, edx		; on success, eax must have been reset to "SMAP"
	jne 	short .failed
	test 	ebx, ebx		; ebx = 0 implies list is only 1 entry long (worthless)
	je 	short .failed
	jmp 	short .jmpin
	
.e820lp:
	mov 	eax, 0xe820		; eax, ecx get trashed on every int 0x15 call
	mov 	[di + 20], dword 1	; force a valid ACPI 3.X entry
	mov 	ecx, 24			; ask for 24 bytes again
	int 	0x15
	jc 	short .e820f		; carry set means "end of list already reached"
	mov 	edx, 0x0534D4150	; repair potentially trashed register
	
.jmpin:
	jcxz 	.skipent		; skip any 0 length entries
	cmp 	cl, 20			; got a 24 byte ACPI 3.X response?
	jbe 	short .notext
	test 	byte [di + 20], 1	; if so: is the "ignore this data" bit clear?
	je 	short .skipent
	
.notext:
	mov 	ecx, [di + 8]		; get lower dword of memory region length
	or 	ecx, [di + 12]		; "or" it with upper dword to test for zero
	jz 	.skipent		; if length qword is 0, skip entry
	inc 	bp			; got a good entry: ++count, move to next storage spot
	add 	di, 24
	
.skipent:
	test	ebx, ebx		; if ebx resets to 0, list is complete
	jne 	short .e820lp
	
.e820f:
	ret
	
.failed:
	; error occured
	mov	si, msgNoMemoryMap
	jmp	PrintError

;*********************************************************
; end of bootsectors
times 1024 - ($ - $$) db 0





































